import './base-imports';

import classnames from 'classnames';
import clone from 'clone';
import CodeMirror, {
  type CodeMirrorLinkClickCallback,
  type EditorChange,
  type EditorConfiguration,
  type ShowHintOptions,
} from 'codemirror';
import type { GraphQLInfoOptions } from 'codemirror-graphql/info';
import type { ModifiedGraphQLJumpOptions } from 'codemirror-graphql/jump';
import deepEqual from 'deep-equal';
import { JSONPath } from 'jsonpath-plus';
import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { Button, Menu, MenuItem, MenuTrigger, Popover, Toolbar } from 'react-aria-components';
import { useLatest, useMount, useUnmount } from 'react-use';
import vkBeautify from 'vkbeautify';

import { DEBOUNCE_MILLIS, isMac } from '~/common/constants';
import * as misc from '~/common/misc';
import type { KeyCombination } from '~/common/settings';
import { getTemplateTags } from '~/plugins';
import { useRootLoaderData } from '~/root';
import { getTagDefinitions } from '~/templating/index';
import { type NunjucksParsedTag, type nunjucksTagContextMenuOptions } from '~/templating/types';
import { extractNunjucksTagFromCoords } from '~/templating/utils';
import { Icon } from '~/ui/components/icon';
import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from '~/ui/components/keydown-binder';
import { FilterHelpModal } from '~/ui/components/modals/filter-help-modal';
import { showModal } from '~/ui/components/modals/index';
import { NunjucksModal } from '~/ui/components/modals/nunjucks-modal';
import { UpgradeModal } from '~/ui/components/modals/upgrade-modal';
import { isKeyCombinationInRegistry } from '~/ui/components/settings/shortcuts';
import { useNunjucks } from '~/ui/context/nunjucks/use-nunjucks';
import { useEditorRefresh } from '~/ui/hooks/use-editor-refresh';
import { usePlanData } from '~/ui/hooks/use-plan';
import { ednPrettify } from '~/utils/prettify/edn';
import { jsonPrettify } from '~/utils/prettify/json';
import { queryXPath } from '~/utils/xpath/query';

import { normalizeIrregularWhitespace } from './normalize-irregular-whitespace';
const TAB_SIZE = 4;
const MAX_SIZE_FOR_LINTING = 1_000_000; // Around 1MB

interface EditorState {
  scroll: CodeMirror.ScrollInfo;
  selections: CodeMirror.Range[];
  cursor: CodeMirror.Position;
  history: any;
  marks: Partial<CodeMirror.MarkerRange>[];
}

export const shouldIndentWithTabs = ({ mode, indentWithTabs }: { mode?: string; indentWithTabs?: boolean }) => {
  // YAML is not valid when indented with Tabs
  const isYaml = mode?.includes('yaml') || false;
  // OpenAPI is not valid when indented with Tabs
  // TODO: OpenAPI in yaml is not valid with tabs, but in JSON is. Currently we do not differentiate and disable tabs regardless. INS-1390
  const isOpenAPI = mode === 'openapi';
  return indentWithTabs && !isYaml && !isOpenAPI;
};

const widget = (cm: CodeMirror.EditorFromTextArea | null, from: CodeMirror.Position, to: CodeMirror.Position) => {
  // Prevent retrieving an invalid content if undefined
  if (!from?.line || !to?.line) {
    return '\u2194';
  }
  const prevLine = cm?.getLine(from.line);
  if (!prevLine) {
    return '\u2194';
  }
  try {
    const squareBraceIsOutsideCurlyBrace = prevLine.lastIndexOf('[') > prevLine.lastIndexOf('{');
    const startToken = squareBraceIsOutsideCurlyBrace ? '[' : '{';
    const endToken = squareBraceIsOutsideCurlyBrace ? ']' : '}';
    const keys = Object.keys(JSON.parse(startToken + cm?.getRange(from, to) + endToken));
    return keys.length ? `\u21A4 ${keys.length} \u21A6` : '\u2194';
  } catch {
    return '\u2194';
  }
};
// Global object used for storing and persisting editor scroll, lint and folding margin states
const editorStates: Record<string, EditorState> = {};
export interface CodeEditorProps {
  autoPrettify?: boolean;
  className?: string;
  defaultValue?: string;
  dynamicHeight?: boolean;
  enableNunjucks?: boolean;
  filter?: string;
  filterHistory?: string[];
  getAutocompleteConstants?: () => string[] | PromiseLike<string[]>;
  getAutocompleteSnippets?: () => CodeMirror.Snippet[];
  hideGutters?: boolean;
  hideLineNumbers?: boolean;
  hintOptions?: ShowHintOptions;
  id: string;
  infoOptions?: GraphQLInfoOptions;
  jumpOptions?: ModifiedGraphQLJumpOptions;
  lintOptions?: Record<string, any>;
  showPrettifyButton?: boolean;
  mode?: string;
  noLint?: boolean;
  noMatchBrackets?: boolean;
  noStyleActiveLine?: boolean;
  // used only for saving env editor state, focusEvent doesn't work well
  onBlur?: (e: FocusEvent) => void;
  onFocus?: (e: Event, editor?: CodeMirror.Editor) => void;
  onChange?: (value: string, changeObj: EditorChange[]) => void;
  onCursorActivity?: (doc: CodeMirror.Editor) => void;
  onPaste?: (value: string) => string;
  onClickLink?: CodeMirrorLinkClickCallback;
  pinToBottom?: boolean;
  placeholder?: string;
  readOnly?: boolean;
  style?: object;
  // NOTE: for caching scroll and marks
  uniquenessKey?: string;
  updateFilter?: (filter: string) => void;
}

const normalizeMimeType = (mode?: string) => {
  const mimeType = mode ? mode.split(';')[0] : 'text/plain';
  if (mimeType.includes('graphql-variables')) {
    return 'application/json';
  } else if (mimeType.includes('graphql')) {
    // Because graphQL plugin doesn't recognize application/graphql content-type
    return 'graphql';
  } else if (mimeType.includes('json')) {
    return 'application/json';
  } else if (mimeType.includes('clojure')) {
    return 'application/edn';
  } else if (mimeType.includes('xml')) {
    return 'application/xml';
  } else if (mimeType.includes('kotlin')) {
    return 'text/x-kotlin';
  } else if (mimeType.includes('yaml')) {
    // code-mirror doesn't recognize text/yaml or application/yaml
    // as a valid mime-type
    return 'yaml';
  }
  return mimeType;
};
export interface CodeEditorHandle {
  setValue: (value: string) => void;
  getValue: () => string;
  scrollToSelection: (chStart: number, chEnd: number, lineStart: number, lineEnd: number) => void;
  selectAll: () => void;
  focus: () => void;
  focusEnd: () => void;
  getCursor: () => CodeMirror.Position | undefined;
  setCursorLine: (lineNumber: number) => void;
  tryToSetOption: (key: keyof EditorConfiguration, value: any) => void;
  hasFocus: () => boolean;
  indexFromPos: (pos?: CodeMirror.Position) => number;
  getDoc: () => CodeMirror.Doc | undefined;
}
export const CodeEditor = memo(
  forwardRef<CodeEditorHandle, CodeEditorProps>(
    (
      {
        autoPrettify,
        className,
        defaultValue,
        dynamicHeight,
        enableNunjucks,
        filter,
        filterHistory,
        getAutocompleteConstants,
        getAutocompleteSnippets,
        hideGutters,
        hideLineNumbers,
        hintOptions,
        id,
        infoOptions,
        jumpOptions,
        lintOptions,
        showPrettifyButton,
        mode,
        noLint,
        noMatchBrackets,
        noStyleActiveLine,
        onFocus,
        onBlur,
        onChange,
        onCursorActivity,
        onPaste,
        onClickLink,
        pinToBottom,
        placeholder,
        readOnly,
        style,
        uniquenessKey,
        updateFilter,
      },
      ref,
    ) => {
      const inputRef = useRef<HTMLInputElement>(null);
      const textAreaRef = useRef<HTMLTextAreaElement>(null);
      const codeMirror = useRef<CodeMirror.EditorFromTextArea | null>(null);
      const [originalCode, setOriginalCode] = useState('');
      const { settings } = useRootLoaderData()!;
      const { isOwner, isEnterprisePlan } = usePlanData();
      const indentSize = settings.editorIndentSize;
      const indentWithTabs = shouldIndentWithTabs({ mode, indentWithTabs: settings.editorIndentWithTabs });
      const indentChars = indentWithTabs ? '\t' : Array.from({ length: (indentSize || TAB_SIZE) + 1 }).join(' ');
      const extraKeys = useMemo(
        () => ({
          'Ctrl-Q': (cm: CodeMirror.Editor) => cm.foldCode(cm.getCursor()),
          [isMac() ? 'Cmd-/' : 'Ctrl-/']: 'toggleComment',
          // Autocomplete
          'Ctrl-Space': 'autocomplete',
          // Change default find command from "find" to "findPersistent" so the
          // search box stays open after pressing Enter
          [isMac() ? 'Cmd-F' : 'Ctrl-F']: 'findPersistent',
          [isMac() ? 'Shift-Cmd--' : 'Shift-Ctrl--']: 'foldAll',
          [isMac() ? 'Shift-Cmd-=' : 'Shift-Ctrl-=']: 'unfoldAll',
          'Shift-Tab': 'indentLess',
          // Indent with tabs or spaces
          // From https://github.com/codemirror/CodeMirror/issues/988#issuecomment-14921785
          'Tab': (cm: CodeMirror.Editor) =>
            cm.somethingSelected() ? cm.indentSelection('add') : cm.replaceSelection(indentChars, 'end'),
        }),
        [indentChars],
      );
      const { handleRender, handleGetRenderContext } = useNunjucks();
      const isNunjucksEnabled = enableNunjucks && handleRender;

      const maybePrettifyAndSetValue = useCallback(
        (code?: string, forcePrettify?: boolean, filter?: string) => {
          const prettifyXML = (code: string, filter?: string) => {
            if (updateFilter && filter) {
              try {
                const results = queryXPath(code, filter);
                code = `<result>${results.map(r => r.outer).join('\n')}</result>`;
              } catch (err) {
                const errorMessage = err instanceof Error ? err.message : String(err);
                // Failed to parse filter (that's ok)
                code = `<error>${errorMessage}</error>`;
              }
            }
            try {
              return vkBeautify.xml(code, indentChars);
            } catch {
              // Failed to parse so just return original
              return code;
            }
          };
          const prettifyJSON = (code: string, filter?: string) => {
            try {
              let jsonString = code;
              if (updateFilter && filter) {
                try {
                  const codeObj = JSON.parse(code);
                  const results = JSONPath({ json: codeObj, path: filter.trim() });
                  jsonString = JSON.stringify(results);
                } catch (err) {
                  console.log('[jsonpath] Error:', err);
                  jsonString = '[]';
                }
              }
              return jsonPrettify(jsonString, indentChars, autoPrettify);
            } catch {
              // That's Ok, just leave it
              return code;
            }
          };
          const prettifyEDN = (code: string) => {
            try {
              return ednPrettify(code);
            } catch {
              return code;
            }
          };
          if (typeof code !== 'string') {
            console.warn('Code editor was passed non-string value', code);
            return;
          }
          const shouldPrettify = forcePrettify || autoPrettify;
          if (shouldPrettify) {
            setOriginalCode(code);
            if (mode?.includes('xml')) {
              code = prettifyXML(code, filter);
            } else if (mode?.includes('json')) {
              code = prettifyJSON(code, filter);
            } else if (mode?.includes('edn')) {
              code = prettifyEDN(code);
            }
          }
          // this prevents codeMirror from needlessly setting the same thing repeatedly (which has the effect of moving the user's cursor and resetting the viewport scroll: a bad user experience)
          const currentCode = codeMirror.current?.getValue();
          if (currentCode === code) {
            return;
          }
          codeMirror.current?.setValue(code || '');
        },
        [autoPrettify, mode, indentChars, updateFilter],
      );

      useDocBodyKeyboardShortcuts({
        beautifyRequestBody: () => {
          if (mode?.includes('json') || mode?.includes('xml')) {
            maybePrettifyAndSetValue(codeMirror.current?.getValue());
          }
        },
      });

      // NOTE: maybe we don't need this anymore? Maybe not.
      const persistState = useCallback(() => {
        if (uniquenessKey && codeMirror.current) {
          const scrollInfo = codeMirror.current.getScrollInfo();
          // ignore invalid scroll positions
          if (scrollInfo.height <= 0 || scrollInfo.width <= 0) {
            return;
          }
          editorStates[uniquenessKey] = {
            scroll: scrollInfo,
            selections: codeMirror.current.listSelections(),
            cursor: codeMirror.current.getCursor(),
            history: codeMirror.current.getHistory(),
            marks: codeMirror.current
              .getAllMarks()
              .filter(mark => mark.__isFold)
              .map((mark): Partial<CodeMirror.MarkerRange> => {
                const markerRange = mark.find();
                return markerRange && 'from' in markerRange
                  ? markerRange
                  : {
                      from: undefined,
                      to: undefined,
                    };
              }),
          };
        }
      }, [uniquenessKey, codeMirror]);

      const initEditor = useCallback(() => {
        if (!textAreaRef.current) {
          return;
        }

        const showGuttersAndLineNumbers = !hideGutters && !hideLineNumbers;

        const transformEnums = (tagDef: NunjucksParsedTag): NunjucksParsedTag[] => {
          if (tagDef.args[0]?.type === 'enum') {
            return (
              tagDef.args[0].options?.map(option => {
                const optionName = misc.fnOrString(option.displayName, tagDef.args);
                const newDef = clone(tagDef);
                newDef.displayName = `${tagDef.displayName} ⇒ ${optionName}`;
                newDef.args[0].defaultValue = option.value;

                return newDef;
              }) || []
            );
          }
          return [tagDef];
        };

        const initialOptions: EditorConfiguration = {
          lineNumbers: showGuttersAndLineNumbers,
          placeholder: placeholder || '',
          foldGutter: showGuttersAndLineNumbers,
          autoRefresh: { delay: 2000 },
          lineWrapping: settings.editorLineWrapping ?? true,
          scrollbarStyle: 'native',
          lint: !noLint && !readOnly,
          matchBrackets: !noMatchBrackets,
          autoCloseBrackets: true,
          tabSize: indentSize || TAB_SIZE,
          indentUnit: indentSize || TAB_SIZE,
          hintOptions,
          info: infoOptions,
          viewportMargin: dynamicHeight ? Infinity : 30,
          readOnly: !!readOnly,
          selectionPointer: 'default',
          jump: jumpOptions,
          styleActiveLine: !noStyleActiveLine,
          indentWithTabs,
          showCursorWhenSelecting: false,
          cursorScrollMargin: 12,
          // Only set keyMap if we're not read-only. This is so things like ctrl-a work on read-only mode.
          keyMap: !readOnly && settings.editorKeyMap ? settings.editorKeyMap : 'default',
          extraKeys: CodeMirror.normalizeKeyMap(extraKeys),
          gutters: showGuttersAndLineNumbers
            ? ['CodeMirror-lint-markers', 'CodeMirror-linenumbers', 'CodeMirror-foldgutter']
            : [],
          foldOptions: {
            widget: (from: CodeMirror.Position, to: CodeMirror.Position) => widget(codeMirror.current, from, to),
          },
          mode: !isNunjucksEnabled ? normalizeMimeType(mode) : { name: 'nunjucks', baseMode: normalizeMimeType(mode) },
          environmentAutocomplete: {
            getVariables: async () => (!handleGetRenderContext ? [] : (await handleGetRenderContext())?.keys || []),
            getTags: async () => (!handleGetRenderContext ? [] : (await getTagDefinitions()).flatMap(transformEnums)),
            getConstants: getAutocompleteConstants,
            getSnippets: getAutocompleteSnippets,
            hotKeyRegistry: settings.hotKeyRegistry,
            autocompleteDelay: settings.autocompleteDelay,
          },
        };
        codeMirror.current = CodeMirror.fromTextArea(textAreaRef.current, initialOptions);
        codeMirror.current.on('beforeChange', (doc: CodeMirror.Editor, change: CodeMirror.EditorChangeCancellable) => {
          const isGraphqlWithChange = doc.getOption('mode') === 'graphql' && change.text.length > 0;
          if (isGraphqlWithChange) {
            // Don't allow non-breaking spaces because they break the GraphQL syntax
            change.update?.(change.from, change.to, change.text.map(normalizeIrregularWhitespace));
          }
          if (pinToBottom) {
            const scrollInfo = doc.getScrollInfo();
            const scrollPosition = scrollInfo.height - scrollInfo.clientHeight;
            doc.scrollTo(0, scrollPosition);
          }

          if (onPaste && change.origin === 'paste' && change.update) {
            const translatedText = onPaste(change.text.join('\n')).split('\n');

            change.update(change.from, change.to, translatedText);
          }
        });

        codeMirror.current.on('change', (doc: CodeMirror.Editor) => {
          if (pinToBottom) {
            const scrollInfo = doc.getScrollInfo();
            const scrollPosition = scrollInfo.height - scrollInfo.clientHeight;
            doc.scrollTo(0, scrollPosition);
          }
        });

        codeMirror.current.on('keydown', (doc: CodeMirror.Editor, event: KeyboardEvent) => {
          const pressedKeyComb: KeyCombination = {
            ctrl: event.ctrlKey,
            alt: event.altKey,
            shift: event.shiftKey,
            meta: event.metaKey,
            keyCode: event.keyCode,
          };

          const isUserDefinedKeyboardShortcut = isKeyCombinationInRegistry(pressedKeyComb, settings.hotKeyRegistry);
          const isAutoCompleteBinding = isKeyCombinationInRegistry(pressedKeyComb, {
            showAutocomplete: settings.hotKeyRegistry.showAutocomplete,
          });
          // Stop the editor from handling global keyboard shortcuts except for the autocomplete binding
          const isShortcutButNotAutocomplete = isUserDefinedKeyboardShortcut && !isAutoCompleteBinding;

          // Should not capture escape in order to exit modals
          const isEscapeKey = event.code === 'Escape';

          if (isShortcutButNotAutocomplete) {
            // @ts-expect-error -- unsound property assignment
            event.codemirrorIgnore = settings.editorKeyMap !== 'vim';
            // Stop the editor from handling the escape key
          } else if (isEscapeKey) {
            // @ts-expect-error -- unsound property assignment
            event.codemirrorIgnore = settings.editorKeyMap !== 'vim';
          } else {
            event.stopPropagation();

            // Enable graphql completion if we're in that mode
            if (doc.getOption('mode') === 'graphql') {
              // Only operate on one-letter keys. This will filter out
              // any special keys (Backspace, Enter, etc)
              const isModifier = event.metaKey || event.ctrlKey || event.altKey || event.key.length > 1;
              // You don't want to re-trigger the hint dropdown if it's already open
              // for other reasons, like forcing its display with Ctrl+Space
              const isDropdownActive = codeMirror.current?.isHintDropdownActive();
              if ((isAutoCompleteBinding || !isModifier) && !isDropdownActive) {
                doc.execCommand('autocomplete');
              }
            }
          }
        });

        codeMirror.current.on('scroll', persistState);
        codeMirror.current.on('fold', persistState);
        codeMirror.current.on('unfold', persistState);
        codeMirror.current.on('keyHandled', (_: CodeMirror.Editor, _keyName: string, event: Event) =>
          event.stopPropagation(),
        );
        codeMirror.current.setCursor({ line: -1, ch: -1 });

        // Actually set the value
        maybePrettifyAndSetValue(defaultValue || '', false, filter);
        // Clear history so we can't undo the initial set
        codeMirror.current?.clearHistory();
        // Setup nunjucks listeners
        if (!readOnly && isNunjucksEnabled && !settings.nunjucksPowerUserMode) {
          codeMirror.current?.enableNunjucksTags(
            handleRender,
            handleGetRenderContext,
            settings.showVariableSourceAndValue,
            id,
          );
        }
        // Make URLs clickable
        if (onClickLink) {
          codeMirror.current.makeLinksClickable(onClickLink);
        }
        // Restore the state
        if (uniquenessKey && editorStates[uniquenessKey]) {
          const { scroll, selections, cursor, history, marks } = editorStates[uniquenessKey];
          codeMirror.current.scrollTo(scroll.left, scroll.top);
          codeMirror.current.setHistory(history);
          // NOTE: These won't be visible unless the editor is focused
          codeMirror.current.setCursor(cursor.line, cursor.ch, { scroll: false });
          codeMirror.current.setSelections(selections, undefined, { scroll: false });
          // Restore marks one-by-one
          for (const { from, to } of marks || []) {
            // @ts-expect-error -- type unsoundness
            codeMirror.current.foldCode(from, to);
          }
        }
        // settings.pluginsAllowElevatedAccess is not used here but we want to trigger this effect when it changes
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [
        hideGutters,
        hideLineNumbers,
        placeholder,
        settings.editorLineWrapping,
        settings.editorKeyMap,
        settings.hotKeyRegistry,
        settings.autocompleteDelay,
        settings.nunjucksPowerUserMode,
        settings.pluginsAllowElevatedAccess,
        settings.showVariableSourceAndValue,
        noLint,
        readOnly,
        noMatchBrackets,
        indentSize,
        hintOptions,
        infoOptions,
        dynamicHeight,
        jumpOptions,
        noStyleActiveLine,
        indentWithTabs,
        extraKeys,
        handleRender,
        mode,
        getAutocompleteConstants,
        getAutocompleteSnippets,
        persistState,
        maybePrettifyAndSetValue,
        defaultValue,
        filter,
        onClickLink,
        uniquenessKey,
        handleGetRenderContext,
        pinToBottom,
        onPaste,
        id,
      ]);

      const cleanUpEditor = useCallback(() => {
        codeMirror.current?.toTextArea();
        codeMirror.current?.closeHintDropdown();
        codeMirror.current = null;
      }, []);

      useMount(() => {
        initEditor();
      });
      useUnmount(() => {
        persistState();
        cleanUpEditor();
      });

      const reinitialize = useCallback(() => {
        cleanUpEditor();
        initEditor();
      }, [cleanUpEditor, initEditor]);

      useEditorRefresh(reinitialize);

      const latestOnChangeRef = useLatest(onChange);

      useEffect(() => {
        const fn = misc.debounce((doc: CodeMirror.Editor, changeObj: EditorChange[]) => {
          if (latestOnChangeRef.current) {
            const value = doc.getValue()?.trim() || '';
            // Disable linting if the document reaches a maximum size or is empty
            const withinLintingThresholds = value.length > 0 && value.length < MAX_SIZE_FOR_LINTING;
            const isLintPropOn = !noLint;
            const shouldLint = withinLintingThresholds && isLintPropOn;
            const lintOption = lintOptions || true;
            try {
              const newValue = shouldLint ? lintOption : false;
              if (!deepEqual(codeMirror.current?.getOption('lint'), newValue)) {
                tryToSetOption('lint', newValue);
              }
            } catch (err) {
              const errorMessage = err instanceof Error ? err.message : String(err);
              console.log('[codemirror] Failed to set CodeMirror option', errorMessage);
            }
            latestOnChangeRef.current(doc.getValue() || '', changeObj);
            setOriginalCode(doc.getValue() || '');
          }
        }, DEBOUNCE_MILLIS);

        codeMirror.current?.on('changes', fn);
        return () => codeMirror.current?.off('changes', fn);
      }, [lintOptions, noLint, latestOnChangeRef]);

      useEffect(() => {
        const handleOnBlur = (_: CodeMirror.Editor, e: FocusEvent) => onBlur?.(e);
        codeMirror.current?.on('blur', handleOnBlur);
        return () => codeMirror.current?.off('blur', handleOnBlur);
      }, [onBlur]);

      useEffect(() => {
        const handleOnFocus = (_: CodeMirror.Editor, e: FocusEvent) => onFocus?.(e, _);
        codeMirror.current?.on('focus', handleOnFocus);
        return () => codeMirror.current?.off('focus', handleOnFocus);
      }, [onFocus]);

      const tryToSetOption = (key: keyof EditorConfiguration, value: any) => {
        try {
          codeMirror.current?.setOption(key, value);
        } catch (err) {
          const errorMessage = err instanceof Error ? err.message : String(err);
          console.log('[codemirror] Failed to set CodeMirror option', errorMessage, { key, value });
        }
      };
      useEffect(() => {
        const unsubscribe = window.main.on(
          'nunjucks-context-menu-command',
          (_, { key, tag, nunjucksTag, needsEnterprisePlan, displayName }) => {
            if (id === key) {
              if (needsEnterprisePlan && !isEnterprisePlan) {
                // show modal if current user is not an enterprise user and the command is an enterprise feature
                showModal(UpgradeModal, {
                  newPlan: 'enterprise',
                  featureName: displayName,
                  isOwner,
                });
                return;
              }
              if (nunjucksTag) {
                const { type, template, range } = nunjucksTag as nunjucksTagContextMenuOptions;
                if (type === 'edit') {
                  showModal(NunjucksModal, {
                    template: template,
                    onDone: (template: string | null) => {
                      const { from, to } = range;
                      codeMirror.current?.replaceRange(template!, from, to);
                    },
                  });
                } else if (type === 'delete') {
                  const { from, to } = range;
                  codeMirror.current?.replaceRange('', from, to);
                } else {
                  return;
                }
              } else {
                codeMirror.current?.replaceSelection(tag);
              }
            }
          },
        );
        return () => {
          unsubscribe();
        };
      }, [id, isEnterprisePlan, isOwner]);
      useEffect(() => tryToSetOption('hintOptions', hintOptions), [hintOptions]);
      useEffect(() => tryToSetOption('info', infoOptions), [infoOptions]);
      useEffect(() => tryToSetOption('jump', jumpOptions), [jumpOptions]);
      // This line will trigger codeMirror lint
      useEffect(() => tryToSetOption('lint', lintOptions), [lintOptions]);
      useEffect(
        () =>
          tryToSetOption(
            'mode',
            !isNunjucksEnabled ? normalizeMimeType(mode) : { name: 'nunjucks', baseMode: normalizeMimeType(mode) },
          ),
        [isNunjucksEnabled, mode],
      );

      useImperativeHandle(
        ref,
        () => ({
          setValue: value => codeMirror.current?.setValue(value),
          getValue: () => codeMirror.current?.getValue() || '',
          selectAll: () =>
            codeMirror.current?.setSelection({ line: 0, ch: 0 }, { line: codeMirror.current.lineCount(), ch: 0 }),
          focus: () => codeMirror.current?.focus(),
          scrollToSelection: (chStart: number, chEnd: number, lineStart: number, lineEnd: number) => {
            codeMirror.current?.setSelection({ line: lineStart, ch: chStart }, { line: lineEnd, ch: chEnd });
            // If sizing permits, position selection just above center
            codeMirror.current?.scrollIntoView({ line: lineStart, ch: chStart }, window.innerHeight / 2 - 100);
          },
          focusEnd: () => {
            if (codeMirror.current && !codeMirror.current.hasFocus()) {
              codeMirror.current.focus();
            }
            codeMirror.current?.getDoc()?.setCursor(codeMirror.current.getDoc().lineCount(), 0);
          },
          getCursor: () => {
            return codeMirror.current?.getCursor();
          },
          setCursorLine: (lineNumber: number) => {
            codeMirror.current?.setCursor(lineNumber);
          },
          tryToSetOption,
          hasFocus: () => codeMirror.current?.hasFocus() as boolean,
          indexFromPos: (pos?: CodeMirror.Position) => (pos ? codeMirror.current?.indexFromPos(pos) || 0 : 0),
          getDoc: () => codeMirror.current?.getDoc(),
        }),
        [],
      );

      useEffect(() => {
        const handleCursorActivity = (doc: CodeMirror.Editor) => {
          onCursorActivity?.(doc);
        };

        const handleFocus = (_: CodeMirror.Editor, event: Event) => {
          onFocus?.(event);
        };
        codeMirror.current?.on('cursorActivity', handleCursorActivity);

        codeMirror.current?.on('focus', handleFocus);

        return () => {
          codeMirror.current?.off('cursorActivity', handleCursorActivity);
          codeMirror.current?.off('focus', handleFocus);
        };
      }, [onCursorActivity, onFocus]);

      const showFilter = readOnly && (mode?.includes('json') || mode?.includes('xml'));
      const showPrettify = (showPrettifyButton && mode?.includes('json')) || mode?.includes('xml');

      return (
        <div
          className={classnames(className, {
            'editor': true,
            'editor--dynamic-height': dynamicHeight,
            'editor--readonly': readOnly,
          })}
          style={style}
          data-editor-type="text"
          data-testid="CodeEditor"
          onContextMenu={async event => {
            if (readOnly || !enableNunjucks) {
              return;
            }
            event.preventDefault();
            const pluginTemplateTags = (await getTemplateTags()).map(tag => ({
              // Skip unsupported objects like functions in template tag to send in IPC
              templateTag: structuredClone(tag.templateTag),
            }));
            const target = event.target as HTMLElement;
            // right click on nunjucks tag
            if (target?.classList?.contains('nunjucks-tag')) {
              const { clientX, clientY } = event;
              const nunjucksTag = extractNunjucksTagFromCoords({ left: clientX, top: clientY }, codeMirror);
              if (nunjucksTag) {
                // show context menu for nunjucks tag
                window.main.showNunjucksContextMenu({ key: id, nunjucksTag, pluginTemplateTags });
              }
            } else {
              window.main.showNunjucksContextMenu({ key: id, pluginTemplateTags });
            }
          }}
        >
          <div
            className={classnames('editor__container', 'input', className)}
            style={{ fontSize: `${settings.editorFontSize}px` }}
          >
            <textarea
              id={id}
              ref={textAreaRef}
              style={{ display: 'none' }}
              readOnly={readOnly}
              autoComplete="off"
              defaultValue=""
            />
          </div>
          {showFilter || showPrettify ? (
            <div
              key={uniquenessKey}
              className="flex h-(--line-height-sm) w-full items-center border-t border-solid border-(--hl-md) text-(--font-size-sm)"
            >
              {showFilter ? (
                <input
                  ref={inputRef}
                  key="filter"
                  type="text"
                  className="flex-1 pl-3"
                  title="Filter response body"
                  defaultValue={filter || ''}
                  placeholder={mode?.includes('json') ? '$.store.books[*].author' : '/store/books/author'}
                  onKeyDown={createKeybindingsHandler({
                    Enter: () => {
                      const filter = inputRef.current?.value;
                      if (updateFilter) {
                        updateFilter(filter || '');
                      }
                      maybePrettifyAndSetValue(originalCode, false, filter);
                    },
                  })}
                  onChange={e => {
                    if (e.target.value === '') {
                      if (updateFilter) {
                        updateFilter('');
                      }
                      maybePrettifyAndSetValue(originalCode, false);
                    }
                  }}
                />
              ) : null}
              <Toolbar className="flex h-full items-center">
                {showFilter && filterHistory && filterHistory.length > 0 && (
                  <MenuTrigger>
                    <Button
                      aria-label="Filter History"
                      className="flex aspect-square h-full items-center justify-center rounded-xs text-sm text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)"
                    >
                      <Icon icon="clock" />
                    </Button>
                    <Popover className="flex min-w-max flex-col overflow-y-hidden">
                      <Menu
                        aria-label="Filter history menu"
                        selectionMode="single"
                        onAction={key => {
                          const index = Number(key);
                          const filter = filterHistory[index];
                          if (inputRef.current) {
                            inputRef.current.value = filter;
                          }
                          if (updateFilter) {
                            updateFilter(filter);
                          }
                          maybePrettifyAndSetValue(originalCode, false, filter);
                        }}
                        items={filterHistory.map((filter, index) => ({
                          id: filter,
                          name: filter,
                          key: index,
                        }))}
                        className="min-w-max overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) py-2 text-sm shadow-lg select-none focus:outline-hidden"
                      >
                        {item => (
                          <MenuItem
                            className="flex h-(--line-height-xs) w-full items-center gap-2 bg-transparent px-(--padding-md) whitespace-nowrap text-(--color-font) transition-colors hover:bg-(--hl-sm) focus:bg-(--hl-xs) focus:outline-hidden disabled:cursor-not-allowed aria-selected:font-bold"
                            aria-label={item.name}
                          >
                            <span>{item.name}</span>
                          </MenuItem>
                        )}
                      </Menu>
                    </Popover>
                  </MenuTrigger>
                )}

                {showFilter ? <FilterHelpModal isJSON={Boolean(mode?.includes('json'))} /> : null}
                {showPrettify ? (
                  <Button
                    key="prettify"
                    className="flex h-full items-center justify-center gap-2 px-4 py-1 text-xs text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)"
                    aria-label="Auto-format request body whitespace"
                    onPress={() => {
                      if (mode?.includes('json') || mode?.includes('xml')) {
                        maybePrettifyAndSetValue(codeMirror.current?.getValue(), true);
                      }
                    }}
                  >
                    Beautify {mode?.includes('json') ? 'JSON' : mode?.includes('xml') ? 'XML' : ''}
                  </Button>
                ) : null}
              </Toolbar>
            </div>
          ) : null}
        </div>
      );
    },
  ),
);
CodeEditor.displayName = 'CodeEditor';
